﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace BlockingQueue
{
	/// <summary>
	/// Thread safe blocking queue. 
	/// When queue is empty all consumer threads, that use Take method are blocked until items added or
	/// CompleteAdding called
	/// When queue with upper-bound capacity is full all producers, that Use Add method are blocked
	/// until free place appeared or CompleteAdding called</summary>
	public class BlockingQueue<T> : IDisposable
	{
		/// <summary>
		/// Object for synchronization locking.</summary>
		private readonly object _locker = new object();

		/// <summary>
		/// Items queue.</summary>
		private readonly Queue<T> _queue;

		/// <summary>
		/// Number of consumers waiting for taking items.</summary>
		private int _consumersWaiting;

		/// <summary>
		/// Number of producers waiting for adding items.</summary>
		private int _producersWaiting;

		/// <summary>
		/// Flag to prevent multiple disposing.</summary>
		volatile private bool _isDisposed;

        /// <summary>
        /// Upper-bound for queue size field.</summary>
	    private int _maxCapacity;
	    /// <summary>
	    /// Upper-bound for queue size property.</summary>
	    public int MaxCapacity
	    {
	        get
	        {
	            CheckDisposed();
	            return _maxCapacity;
	        }
            private set { _maxCapacity = value; }
	    }

        /// <summary>
        /// Flag-field for checking if adding is closed.</summary>
        private bool _isAddingCompleted;
	    /// <summary>
	    /// Flag-property for checking if adding is closed.</summary>
	    public bool IsAddingCompleted
	    {
	        get
	        {
                lock (_locker)
                {
                    CheckDisposed();
                    return _isAddingCompleted;
                }
	        }
            private set
            {
                lock (_locker)
                {
                    CheckDisposed();
                    _isAddingCompleted = value;
                }
            }
	    }

		/// <summary>
		/// Prevent queue from future additions. Unlocks all waiting consumers and producers</summary>
		public void CompleteAdding()
		{
			lock (_locker)
			{
                CheckDisposed();
                if (IsAddingCompleted)
                    return;
				IsAddingCompleted = true;
				Monitor.PulseAll(_locker);
			}
		}

		/// <summary>
		/// Flag for checking if adding is closed and queue is empty. 
		/// No more operations with queue can be taken</summary>
	    public bool IsCompleted
	    {
	        get
	        {
                lock (_locker)
                {
                    CheckDisposed();
                    return IsAddingCompleted && (Count == 0);
                }
	        }
	    }

	    /// <summary>
		/// Count of items in queue.</summary>
		public int Count
		{
			get
			{
                lock (_locker)
                {
                    CheckDisposed();
                    return _queue.Count;
                }
			}
		}

		/// <summary>
		/// Constructor without bounding Queue size.</summary>
		public BlockingQueue()
			:this(Int32.MaxValue)
		{
		}

		/// <summary>
		/// Constructor with bounding queue size.</summary>
		/// <param name="maxCapacity"> Upper-bound for queue size.</param>
		public BlockingQueue(int maxCapacity)
		{
			const int minAllowedCapacity = 1;
			if (maxCapacity < minAllowedCapacity)
				throw new ArgumentOutOfRangeException("maxCapacity", maxCapacity, "BlockingQueue: invalid max capacity range");

		    IsAddingCompleted = false;
			MaxCapacity = maxCapacity;
			_queue = maxCapacity == int.MaxValue 
				? new Queue<T>() 
				: new Queue<T>(maxCapacity);
		}

		/// <summary>
		/// Adds new item to the end of queue.</summary>
		/// <param name="item"> Item for adding</param>
		/// <returns>
		/// true if succeded. false if adding was completed or max capacity reached</returns>
		public bool TryAdd(T item)
		{
			lock (_locker)
			{
                CheckDisposed();
                if (IsAddingCompleted)
                    throw new InvalidOperationException("BlockingQueue: can't add - queue is marked as completed");

				if (_queue.Count >= MaxCapacity)
					return false;
				
				_queue.Enqueue(item);
				if (_consumersWaiting > 0)
					Monitor.PulseAll(_locker);
			}
			return true;
		}

		/// <summary>
		/// Adds new item to the end of queue. Waits if queue has max allowed size.</summary>
		/// <param name="item"> Item for adding</param>
		public void Add(T item)
		{
			lock (_locker)
			{
				while (!TryAdd(item))
				{
					_producersWaiting++;
					Monitor.Wait(_locker);
					_producersWaiting--;
				}
			}
		}

		/// <summary>
		/// Takes item from queue.</summary>
		/// <param name="item"> Item taken from queue</param>
		/// <returns>
		/// true if succeded. false if no items in queue</returns>
		public bool TryTake(out T item)
		{
			lock (_locker)
			{
                CheckDisposed();
                item = default(T);

				if (_queue.Count == 0)
					return false;
				
				item = _queue.Dequeue();
				if (_producersWaiting > 0)
					Monitor.PulseAll(_locker);
			}
			return true;
		}

		/// <summary>
		/// Takes item from queue. Waits if queue is empty.</summary>
		/// <returns>
		/// Item taken from queue</returns>
		public T Take()
		{
			T item;
			lock (_locker)
			{
				while (!TryTake(out item))
				{
					if (IsCompleted)
						throw new InvalidOperationException("BlockingQueue: can't take when done");

					_consumersWaiting++;
					Monitor.Wait(_locker);
					_consumersWaiting--;
				}
			}
			return item;
		}

		/// <summary>
		/// Checks instance has been disposed.</summary>
		private void CheckDisposed()
		{
			if (_isDisposed)
				throw new ObjectDisposedException("BlockingQueue", "BlockingQueue is disposed");
		}

		/// <summary>
		/// Releases all resources used by the current instance of the BlockingQueue class.</summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Marks queue as disposed and unblocks all waiting threads</summary>
		/// <param name="disposing"> Do dispose flag</param>
		protected virtual void Dispose(bool disposing)
		{
			lock (_locker)
			{
                if (_isDisposed) return;
                CompleteAdding();

				_queue.Clear();
                _isDisposed = true;
			}
		}
    }
}
